package sf.database.template.mybatis;

import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.EnumOrdinalTypeHandler;
import org.apache.ibatis.type.EnumTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.apache.ibatis.type.UnknownTypeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sf.core.DBObject;
import sf.spring.util.CollectionUtils;
import sf.tools.StringUtils;

import javax.persistence.Column;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * mybatis 模版主要代码
 */
public class MybatisTemplate {
    private static Logger logger = LoggerFactory.getLogger(MybatisTemplate.class);

    static LanguageDriver languageDriver = new XMLLanguageDriver();

    /**
     * 生成sql语句对象
     * @param configuration 配置
     * @param sqlInXml      sql的xml信息
     * @param parameter     参数
     * @return sql语句
     */
    @SuppressWarnings("unchecked")
    public static SqlParam generate(Configuration configuration, String sqlInXml, Map<String, Object> parameter) {
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        SqlParam sqlWithParam = new SqlParam();
        if (StringUtils.isBlank(sqlInXml)) {
            sqlWithParam.setSql(null);
            sqlWithParam.setParams(null);
            return sqlWithParam;
        }
        sqlInXml = sqlInXml.trim();
        if (sqlInXml.contains("</")) {
            sqlInXml = "<script>" + sqlInXml + "</script>";
            sqlInXml = StringUtils.replaceEach(sqlInXml, new String[]{" > ", " < ", " >= ", " <= ", " <> "},
                    new String[]{" &gt; ", " &lt; ", " &gt;= ", " &lt;= ", " &lt;&gt; "});//模板语句里需要注意空格,否则容易报错.
        }
        if (null == parameter) {
            parameter = new LinkedHashMap<>();
        }
        String id = null;
        Properties properties = new Properties();
        for (Map.Entry<String, Object> entry : parameter.entrySet()) {
            if (null == entry.getValue()) {
                continue;
            }
            properties.put(entry.getKey(), entry.getValue());
        }
        configuration.setVariables(properties);
        BoundSql boundSql = null;
        try {
            SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlInXml, Map.class);
            boundSql = sqlSource.getBoundSql(parameter);
        } catch (Exception e) {
            logger.error("解析sqlxml出错", e);
            logger.error("sqlxml:{}", sqlInXml);
            sqlWithParam.setSql(null);
            sqlWithParam.setParams(Collections.emptyList());
            return sqlWithParam;
        }

        if (boundSql.getSql().contains("#{")) {
            return generate(configuration, boundSql.getSql(), parameter);
        }
        List<ParameterMapping> list = boundSql.getParameterMappings();
        List<Object> argList = new ArrayList<>();
        List<String> propertyNames = new ArrayList<>();
        if (null != list && !list.isEmpty()) {
            Object param = boundSql.getParameterObject();
            for (ParameterMapping parameterMapping : list) {
                Object value;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (param == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(param.getClass())) {
                    value = param;
                } else {
                    MetaObject metaObject = configuration.newMetaObject(param);
                    if (!metaObject.hasGetter(propertyName)) {
                        throw new RuntimeException(id + " " + propertyName + " placeholder not found");
                    }
                    value = metaObject.getValue(propertyName);
                }
                argList.add(value);
                propertyNames.add(propertyName);
            }
        }
        String sql = boundSql.getSql();
        sqlWithParam.setSql(sql);
        sqlWithParam.setParams(argList);
        sqlWithParam.setPropertyNames(propertyNames);
        return sqlWithParam;
    }

    /**
     * 获取绑定的sql,该方法适合configuration已存在模板的情况(主要是为了接管mybatis已有的sql语句),适合老系统的迁移
     * @param configuration 配置
     * @param statement     sqlId
     * @param parameter     参数  可以为null,一般为map类型
     * @return sql信息
     * @see org.apache.ibatis.scripting.defaults.DefaultParameterHandler
     */
    public static SqlParam getSqlParam(Configuration configuration, String statement, Object parameter) {
        MappedStatement ms = configuration.getMappedStatement(statement);
        BoundSql boundSql = ms.getBoundSql(wrapCollection(parameter));
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        //以下为获取sql信息
        List<ParameterMapping> list = boundSql.getParameterMappings();
        List<Object> argList = new ArrayList<>();
        List<String> propertyNames = new ArrayList<>();
        if (null != list && !list.isEmpty()) {
            Object param = boundSql.getParameterObject();
            for (ParameterMapping parameterMapping : list) {
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (param == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(param.getClass())) {
                        value = param;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(param);
                        if (!metaObject.hasGetter(propertyName)) {
                            throw new RuntimeException(boundSql.getSql() + " ====> " + propertyName + " placeholder not found");
                        }
                        value = metaObject.getValue(propertyName);
                    }
                    argList.add(value);
                    propertyNames.add(propertyName);
                }
            }
        }
        String sql = boundSql.getSql();
        SqlParam sqlWithParam = new SqlParam();
        sqlWithParam.setSql(sql);
        sqlWithParam.setParams(argList);
        sqlWithParam.setPropertyNames(propertyNames);
        return sqlWithParam;
    }

    private static Object wrapCollection(final Object object) {
        if (object instanceof Collection) {
            StrictMap<Object> map = new StrictMap<Object>();
            map.put("collection", object);
            if (object instanceof List) {
                map.put("list", object);
            }
            return map;
        } else if (object != null && object.getClass().isArray()) {
            StrictMap<Object> map = new StrictMap<Object>();
            map.put("array", object);
            return map;
        }
        return object;
    }

    public static class StrictMap<V> extends HashMap<String, V> {

        private static final long serialVersionUID = -5741767162221585340L;

        @Override
        public V get(Object key) {
            if (!super.containsKey(key)) {
                throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + this.keySet());
            }
            return super.get(key);
        }

    }

    public static void setOrmType(Configuration configuration) {
        for (MappedStatement ms : configuration.getMappedStatements()) {
            for (ResultMap rm : ms.getResultMaps()) {
                if (rm.getType().isArray()) {

                } else if (Map.class.isAssignableFrom(rm.getType())) {

                } else if (Set.class.isAssignableFrom(rm.getType())) {

                } else if (List.class.isAssignableFrom(rm.getType()) || Collection.class.isAssignableFrom(rm.getType())) {

                } else if (rm.getType().isPrimitive() || rm.getType().isEnum() || rm.getType().getName().startsWith("java.")) {

                } else {
                    if (rm.getType().getAnnotation(MybatisResultClass.class) != null) {
                        List<ResultMapping> resultMappingList = getResultMappingList(rm.getType(), configuration);
                        if (CollectionUtils.isNotEmpty(resultMappingList)) {
                            MetaObject metaObject = configuration.newMetaObject(rm);
                            metaObject.setValue("resultMappings", resultMappingList);
                            metaObject.setValue("idResultMappings", resultMappingList);
                        }
                    }
                }
            }
        }
    }

    /**
     * @param configuration
     * @param clazz
     */
    public static void addResultMap(Configuration configuration, Class<?> clazz) {
        addResultMap(configuration, clazz, null);
    }

    /**
     * 添加resultMap,避免resultMap写在XML中
     * @param configuration
     * @param clazz
     * @param resultMapName 如果为null,将使用全限定类路径.
     */
    public static void addResultMap(Configuration configuration, Class<?> clazz, String resultMapName) {
        if (resultMapName == null) {
            resultMapName = clazz.getName();
        }
        if (!configuration.hasResultMap(resultMapName)) {
            ResultMap rm = getResultMap(clazz, configuration, resultMapName);
            if (rm != null) {
                configuration.addResultMap(rm);
            }
        }
    }

    /**
     * 生成当前实体的resultMap对象
     * @param clazz
     * @param configuration
     * @param resultMapName 如果为null,将使用全限定类路径.
     * @return
     */
    public static ResultMap getResultMap(Class<?> clazz, Configuration configuration, String resultMapName) {
        if (resultMapName == null) {
            resultMapName = clazz.getName();
        }
        List<ResultMapping> resultMappings = getResultMappingList(clazz, configuration);
        ResultMap.Builder builder = new ResultMap.Builder(configuration, resultMapName, clazz, resultMappings, true);
        return builder.build();
    }

    public static List<ResultMapping> getResultMappingList(Class<?> clazz, Configuration configuration) {
        List<Field> fields = new ArrayList<>();
        Collections.addAll(fields, clazz.getDeclaredFields());
        //提取父类字段
        Class<?> superClazz = clazz.getSuperclass();
        for (; superClazz != null && superClazz != Object.class && superClazz != DBObject.class; superClazz = superClazz.getSuperclass()) {
            Collections.addAll(fields, superClazz.getDeclaredFields());
        }

        if (fields.size() == 0) {
            return Collections.emptyList();
        }
        List<ResultMapping> resultMappings = new ArrayList<>();
        handleFields(configuration, fields, resultMappings);
        return resultMappings;
    }

    private static void handleFields(Configuration configuration, List<Field> fields, List<ResultMapping> resultMappings) {
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        // 解析字段 不支持方法上加注解
        if (CollectionUtils.isNotEmpty(fields)) {
            for (Iterator<Field> it = fields.iterator(); it.hasNext(); ) {
                Field f = it.next();
                if (Modifier.isStatic(f.getModifiers()) || Modifier.isTransient(f.getModifiers())) {
                    it.remove();
                }
            }
            for (Field f : fields) {
                Transient trans = f.getAnnotation(Transient.class);
                if (trans != null) {
                    continue;
                }

                //JPA注解处理
                Id id = f.getAnnotation(Id.class);
                Column col = f.getAnnotation(Column.class);
                Temporal temporal = f.getAnnotation(Temporal.class);
                Enumerated enumerated = f.getAnnotation(Enumerated.class);
                Lob lob = f.getAnnotation(Lob.class);

                MybatisResult result = f.getAnnotation(MybatisResult.class);

                Class<?> fieldType = f.getType();
                String columnName = f.getName();
                if (col != null) {
                    if (StringUtils.isNotBlank(col.name())) {
                        columnName = col.name();
                    }
                }
                String property = f.getName();
                JdbcType jdbcType = null;
                Class<? extends TypeHandler> typeHandlerClass = null;
                TypeHandler<?> typeHandler = null;
                if (result != null) {
                    Class<?> javaType = fieldType;
                    if (result.javaType() != void.class) {
                        javaType = result.javaType();
                    }
                    if (result.jdbcType() != JdbcType.UNDEFINED) {
                        jdbcType = result.jdbcType();
                    }
                    if (result.typeHandler() != UnknownTypeHandler.class) {
                        typeHandlerClass = result.typeHandler();
                    }
                    if (typeHandlerClass != null) {
                        try {
                            typeHandler = typeHandlerRegistry.getInstance(javaType, typeHandlerClass);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                    if (typeHandler == null && jdbcType != null) {
                        typeHandler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType);
                    }
                } else if (fieldType.isEnum()) {
                    if (enumerated != null && enumerated.value() == EnumType.ORDINAL) {
                        typeHandler = (TypeHandler<?>) new EnumOrdinalTypeHandler(fieldType);
                    } else {
                        typeHandler = (TypeHandler<?>) new EnumTypeHandler(fieldType);
                    }
                } else if (lob != null) {
                    if (fieldType == String.class || fieldType == CharSequence.class) {
                        // String char
                        typeHandler = typeHandlerRegistry.getTypeHandler(String.class, JdbcType.LONGVARCHAR);
                    }
                }
                if (Date.class.isAssignableFrom(fieldType)) {
                    if (temporal != null) {
                        if (temporal.value() == TemporalType.TIME) {
                            typeHandler = typeHandlerRegistry.getTypeHandler(Date.class, JdbcType.TIME);
                        } else if (temporal.value() == TemporalType.TIMESTAMP) {
                            typeHandler = typeHandlerRegistry.getTypeHandler(Date.class, JdbcType.TIMESTAMP);
                        } else if (temporal.value() == TemporalType.DATE) {
                            typeHandler = typeHandlerRegistry.getTypeHandler(Date.class, JdbcType.DATE);
                        }
                    } else {
                        //默认为时间戳
                        typeHandler = typeHandlerRegistry.getTypeHandler(Date.class, JdbcType.TIMESTAMP);
                    }
                }
                if (typeHandler == null) {
                    typeHandler = typeHandlerRegistry.getTypeHandler(fieldType);
                }
                if (typeHandler == null) {
                    logger.warn("No typehandler found for property " + f.getName());
                    continue;
                }
                ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property, columnName, fieldType);
                if (jdbcType != null) {
                    builder.jdbcType(jdbcType);
                }
                builder.typeHandler(typeHandler);
                List<ResultFlag> flags = new ArrayList<>();
                if (id != null) {
                    flags.add(ResultFlag.ID);
                }
                builder.flags(flags);
                resultMappings.add(builder.build());
            }
        }
    }
}
